# Tests for the server's websocket frontend. The protocol is documented here:
#
# https://github.com/josephg/ShareJS/wiki/Wire-Protocol

testCase = require('nodeunit').testCase
assert = require 'assert'
WebSocketClient = require('websocket').client;
{EventEmitter} = require('events')
server = require '../src/server'
types = require '../src/types'

helpers = require './helpers'
newDocName = helpers.newDocName
applyOps = helpers.applyOps
makePassPart = helpers.makePassPart

ANYOBJECT = new Object

# Helper method to check that subsequent data received by the callback is a particular
# set of values.
expectData = (socket, expectedData, callback) ->
  expectedData = [expectedData] unless Array.isArray expectedData

  socket.onmessage = (data) ->
      expected = expectedData.shift()
      if expected.meta == ANYOBJECT
        assert.strictEqual typeof data.meta, 'object'
        delete data.meta
        delete expected.meta
      assert.deepEqual expected, data
      if expectedData.length == 0
        socket.onmessage = (data) -> console.warn 'xxxx', data   
        callback()

class WSSocket

  constructor: (url, callback)->
    # Open a new websocket session to the server
    ws = new WebSocketClient
    ws.connect url
   
    ws.on 'connectFailed', (error) -> console.error "Failed to connect: #{error}"

    ws.on 'connect', (@connection) => callback(@)

    @__defineSetter__ "onerror", (callback) =>
      listeners = @connection.listeners('error')
      listeners.splice(0, listeners.length)
      @connection.on 'error', (err) => callback(err)

    @__defineSetter__ "onmessage", (callback) =>
      @connection.removeAllListeners "message"
      @connection.on 'message', (data) =>
        callback JSON.parse(data.utf8Data)

    @__defineSetter__ "onclose", (callback) =>
      listeners = @connection.listeners('close')
      listeners.splice(0, listeners.length)
      @connection.on 'close', (err) => callback(err)


  send: (msg) -> @connection.send JSON.stringify(msg)
  
  close: -> @connection.close()


module.exports = testCase
  setUp: (callback) ->
    @auth = (client, action) -> action.accept()

    options =
      websocket: {}
      sockjs: null
      browserchannel: null
      rest: null
      socketio: null
      db: {type: 'none'}
      auth: (client, action) => @auth client, action

    try
      @model = server.createModel options
      @server = server options, @model

      @server.listen =>
        @name = 'testingdoc'

        @socket = new WSSocket "ws://localhost:#{@server.address().port}/websocket", (socket) =>
          socket.onmessage = (data) =>
            @id = data.auth
            assert.ok @id
            callback()
        
          socket.onerror = (e) -> console.warn 'eeee', e
          socket.send({auth:null});

          @expect = (data, callback) =>
            expectData socket, data, callback

    catch e
      console.log e.stack
      throw e
  
  tearDown: (callback) ->
    @socket.close()

    # Its important the port has closed before the next test is run.
    @server.on 'close', callback
    @server.close()

  'open an existing document with no version specified opens the document': (test) ->  
    @model.create @name, 'simple', =>
      @socket.send {doc:@name, open:true}
      @expect {doc:@name, v:0, open:true}, =>
        @model.applyOp @name, {op:{position:0, text:'hi'}, v:0}, =>
          @expect {v:0, op:{position:0, text:'hi'}, meta:ANYOBJECT}, ->
            test.done()

   'open an existing document with version specified opens the document': (test) ->
      @model.create @name, 'simple', =>
        @socket.send {doc:@name, open:true, v:0}
        @expect {doc:@name, v:0, open:true}, =>
          @model.applyOp @name, {op:{position:0, text:'hi'}, v:0}, =>
            @expect {v:0, op:{position:0, text:'hi'}, meta:ANYOBJECT}, ->
              test.done()
    
    'open a nonexistant document with create:true creates the document': (test) ->
      @socket.send {doc:@name, open:true, create:true, type:'simple'}
      @expect {doc:@name, open:true, create:true, v:0}, =>
        @model.getSnapshot @name, (error, docData) ->
          test.ok docData.meta
          test.strictEqual typeof(docData.meta.ctime), 'number'
          test.strictEqual typeof(docData.meta.mtime), 'number'
          delete docData.meta
          test.deepEqual docData, {snapshot:{str:''}, v:0, type:types.simple}
          test.done()

    'open a nonexistant document without create fails': (test) ->
      @socket.send {doc:@name, open:true}
      @expect {doc:@name, open:false, error:'Document does not exist'}, =>
        test.done()

    'open a nonexistant document at a particular version without create fails': (test) ->
      @socket.send {doc:@name, open:true, v:0}
      @expect {doc:@name, open:false, error:'Document does not exist'}, =>
        test.done()
    
    'open a nonexistant document with snapshot:null fails normally': (test) ->
      @socket.send {doc:@name, open:true, snapshot:null}
      @expect {doc:@name, open:false, snapshot:null, error:'Document does not exist'}, =>
        test.done()

    'get a snapshot of a nonexistant document fails normally': (test) ->
      @socket.send {doc:@name, snapshot:null}
      @expect {doc:@name, snapshot:null, error:'Document does not exist'}, =>
        test.done()
    
    'open a nonexistant document with create:true and snapshot:null does not return the snapshot': (test) ->
      # The snapshot can be inferred.
      @socket.send {doc:@name, open:true, create:true, type:'text', snapshot:null}
      @expect {doc:@name, open:true, create:true, v:0}, =>
        test.done()

    'open a document with a different type fails': (test) ->
      @model.create @name, 'simple', =>
        @socket.send {doc:@name, open:true, type:'text'}
        @expect {doc:@name, open:false, error:'Type mismatch'}, =>
          test.done()
    
    'open an existing document with create:true opens the current document': (test) ->
      @model.create @name, 'simple', =>
        @model.applyOp @name, {op:{position:0, text:'hi'}, v:0}, =>
          @socket.send {doc:@name, open:true, create:true, type:'simple', snapshot:null}
          # The type isn't sent if it can be inferred.
          @expect {doc:@name, create:false, open:true, v:1, snapshot:{str:'hi'}}, ->
            test.done()

    'open a document at a previous version and get ops since': (test) ->
      @model.create @name, 'simple', =>
        @model.applyOp @name, {op:{position:0, text:'hi'}, v:0}, =>
          @socket.send {doc:@name, v:0, open:true, type:'simple'}
          @expect [{doc:@name, v:0, open:true}, {v:0, op:{position:0, text:'hi'}, meta:ANYOBJECT}], ->
            test.done()

    'create a document without opening it': (test) ->
      @socket.send {doc:@name, create:true, type:'simple'}
      @expect {doc:@name, create:true}, =>
        @model.getSnapshot @name, (error, docData) ->
          test.ok docData.meta
          delete docData.meta
          test.deepEqual docData, {snapshot:{str:''}, v:0, type:types.simple}
          test.done()
    
    'create a document that already exists returns create:false': (test) ->
      @model.create @name, 'simple', =>
        @socket.send {doc:@name, create:true, type:'simple'}
        @expect {doc:@name, create:false}, =>
          test.done()

    'create a document with snapshot:null returns create:true and no snapshot': (test) ->
      @socket.send {doc:@name, create:true, type:'simple', snapshot:null}
      @expect {doc:@name, create:true}, =>
        test.done()

    'receive ops through an open document': (test) ->
      @socket.send {doc:@name, v:0, open:true, create:true, type:'simple'}
      @expect {doc:@name, v:0, open:true, create:true}, =>
        @model.applyOp @name, {op:{position:0, text:'hi'}, v:0}

        @expect {v:0, op:{position:0, text:'hi'}, meta:ANYOBJECT}, ->
          test.done()

    'send an op': (test) ->
      @model.create @name, 'simple', =>
        listener = (opData) ->
          test.strictEqual opData.v, 0
          test.deepEqual opData.op, {position:0, text:'hi'}
          test.done()
        @model.listen @name, listener, (error, v) -> test.strictEqual v, 0

        @socket.send {doc:@name, v:0, op:{position:0, text:'hi'}}

    'send an op with metadata': (test) ->
      @model.create @name, 'simple', =>
        listener = (opData) ->
          test.strictEqual opData.v, 0
          test.strictEqual opData.meta.x, 5
          test.deepEqual opData.op, {position:0, text:'hi'}
          test.done()
        @model.listen @name, listener, (error, v) -> test.strictEqual v, 0

        @socket.send {doc:@name, v:0, op:{position:0, text:'hi'}, meta:{x:5}}

    'receive confirmation when an op is sent': (test) ->
      @model.create @name, 'simple', =>
        @socket.send {doc:@name, v:0, op:{position:0, text:'hi'}, meta:{x:5}}

        @expect {doc:@name, v:0}, ->
          test.done()

    'not be sent your own ops back': (test) ->
  #    @socket.onmessage = (data) ->
  #      test.notDeepEqual data.op, {position:0, text:'hi'} if data.op?
  #
      @socket.send {doc:@name, open:true, create:true, type:'simple'}
      @socket.send {doc:@name, v:0, op:{position:0, text:'hi'}}

      @expect [{doc:@name, v:0, open:true, create:true}, {v:0}], =>
        # Gonna do this a dodgy way. Because I don't want to wait an undefined amount of time
        # to make sure the op doesn't come, I'll trigger another op and make sure it recieves that.
        # The second op should come after the first.
        @expect {v:1, op:{position:0, text:'yo '}, meta:ANYOBJECT}, ->
          test.done()

        @model.applyOp @name, {v:1, op:{position:0, text:'yo '}}

    'detect duplicate ops using dupIfSource': (test) ->
      @socket.send {doc:@name, open:true, create:true, type:'simple'}
      @socket.send {doc:@name, v:0, op:{position:0, text:'hi'}}
      # Resending the op... In reality, this would be sent in a new session.
      @socket.send {doc:@name, v:0, op:{position:0, text:'hi'}, dupIfSource:[@id]}

      @expect [{doc:@name, v:0, open:true, create:true}, {v:0}, {v:null, error:'Op already submitted'}], ->
        test.done()

    'get a document snapshot': (test) ->
      @model.create @name, 'simple', =>
        @model.applyOp @name, {v:0, op:{position:0, text:'internet'}}, (error, _) =>
          test.ifError(error)

          @socket.send {doc:@name, snapshot:null}
          @expect {doc:@name, snapshot:{str:'internet'}, v:1, type:'simple'}, ->
            test.done()

    'be able to close a document': (test) ->
      name1 = newDocName()
      name2 = newDocName()

      @socket.send {doc:name1, open:true, create:true, type:'simple'}
      @socket.send {open:false}
      @socket.send {doc:name2, open:true, create:true, type:'text'}

      @expect [{doc:name1, open:true, create:true, v:0}, {open:false}, {doc:name2, open:true, create:true, v:0}], =>
        # name1 should be closed, and name2 should be open.
        # We should only get the op for name2.
        @model.applyOp name1, {v:0, op:{position:0, text:'Blargh!'}}, (error, appliedVersion) ->
          test.fail error if error
        @model.applyOp name2, {v:0, op:[{i:'hi', p:0}]}, (error, appliedVersion) ->
          test.fail error if error

        @expect {v:0, op:[{i:'hi', p:0}], meta:ANYOBJECT}, ->
          test.done()
    
    'doc names are sent in ops when necessary': (test) ->
      name1 = newDocName()
      name2 = newDocName()

      @socket.send {doc:name1, open:true, create:true, type:'simple'}
      @socket.send {doc:name2, open:true, create:true, type:'simple'}

      passPart = makePassPart test, 3

      @expect [{doc:name1, open:true, create:true, v:0}, {doc:name2, open:true, create:true, v:0}], =>
        @model.applyOp name1, {v:0, op:{position:0, text:'a'}}, (error) =>
          test.fail error if error
          @model.applyOp name2, {v:0, op:{position:0, text:'b'}}, (error) =>
            test.fail error if error
            @model.applyOp name1, {v:1, op:{position:0, text:'c'}}, (error) =>
              test.fail error if error

        # All the ops that come through the socket should have the doc name set.
        @socket.onmessage = (data) =>
          test.strictEqual data.doc?, true
          passPart()

    "don't repeat document names": (test) ->
      passPart = makePassPart test, 3
      @socket.send {doc:@name, open:true, create:true, type:'simple'}
      @expect {doc:@name, open:true, create:true, v:0}, =>
        @socket.onmessage = (data) =>
          # This time, none of the ops should have the document name set.
          test.strictEqual data.doc?, false
          passPart()

        @socket.send {doc:@name, op:{position: 0, text:'a'}, v:0}
        @socket.send {doc:@name, op:{position: 0, text:'b'}, v:1}
        @socket.send {doc:@name, op:{position: 0, text:'c'}, v:2}

    'an error message is sent through the socket if the operation is invalid': (test) ->
      @model.create @name, 'simple', =>
        # This might cause the model code to print out an error stack trace
        @socket.send {doc:@name, v:0, op:{position:-100, text:'asdf'}}
        @expect {doc:@name, v:null, error:'Invalid position'}, ->
          test.done()

    'creating a document with a null doc name creates a new doc': (test) ->
      @socket.send {doc:null, create:true, type:'simple'}
      @socket.onmessage = (data) =>
        test.strictEqual data.create, true
        test.equal typeof data.doc, 'string'
        test.ok data.doc.length > 8

        @model.getSnapshot data.doc, (error, docData) ->
          test.ok docData.meta
          test.strictEqual typeof(docData.meta.ctime), 'number'
          test.strictEqual typeof(docData.meta.mtime), 'number'
          delete docData.meta
          test.deepEqual docData, {snapshot:{str:''}, v:0, type:types.simple}
          test.done()

  # ---- User agent tests
    'The user agent is persisted across requests': (test) ->
      a = null

      @auth = (agent, action) =>
        if a
          test.strictEqual a, agent
        else
          a = agent
        action.accept()

      @socket.send {doc:@name, open:true, create:true, type:'simple'}
      @expect {doc:@name, open:true, create:true, v:0}, =>
        @socket.send {doc:@name, v:0, op:{position:0, text:'hi'}, meta:{x:5}}
        @expect {v:0}, ->
          test.expect 3
          test.done()

    'Cannot connect if auth rejects you': (test) ->
      @auth = (agent, action) ->
        test.strictEqual action.type, 'connect'
        # TODO - find the right way to access and populate remoteAddress
        # test.ok agent.remoteAddress in ['localhost', '127.0.0.1'] # Is there a nicer way to do this?
        test.strictEqual typeof agent.sessionId, 'string'
        test.ok agent.sessionId.length > 5
        test.ok agent.connectTime

        test.strictEqual typeof agent.headers, 'object'

        # TODO - Get websocket to put user-agent header in the connection
        #test.strictEqual agent.headers['user-agent'], undefined #'node.js'

        action.reject()

      new WSSocket "ws://localhost:#{@server.address().port}/websocket", (socket) ->
        socket.send({auth:null});
        expectData socket, {auth:null, error:'forbidden'}, ->
          socket.onclose = ->
            test.expect 5
            test.done()

    'Cannot open a document if auth rejects you': (test) ->
      @auth = (agent, action) =>
        if action.name == 'open'
          action.reject()
        else
          action.accept()

      @model.create @name, 'simple', =>
        @socket.send {doc:@name, open:true}
        @expect {doc:@name, open:false, error:'forbidden'}, ->
          test.done()

    'Cannot open a document if you cannot get a snapshot': (test) ->
      @auth = (agent, action) =>
        if action.name == 'get snapshot'
          action.reject()
        else
          action.accept()

      @model.create @name, 'simple', =>
        @socket.send {doc:@name, open:true, snapshot:null}
        @expect {doc:@name, open:false, snapshot:null, error:'forbidden'}, ->
          test.done()

    'Cannot create a document if youre not allowed to create': (test) ->
      @auth = (agent, action) =>
        if action.name == 'create'
          action.reject()
        else
          action.accept()

      @socket.send {doc:@name, open:true, create:true, type:'simple'}
      @expect {doc:@name, open:false, error:'forbidden'}, ->
        test.done()

    'Cannot submit an op if auth rejects you': (test) ->
      @auth = (agent, action) ->
        if action.type == 'update'
          action.reject()
        else
          action.accept()

      @socket.send {doc:@name, open:true, create:true, type:'simple', snapshot:null}
      @expect {doc:@name, open:true, create:true, v:0}, =>
        @socket.send {doc:@name, v:0, op:{position:0, text:'hi'}, meta:{}}
        @expect {v:null, error:'forbidden'}, ->
          test.done()

    'Authentication string available in auth function': (test) ->
      @auth = (agent, action) ->
        test.strictEqual agent.authentication, '1234'
        test.strictEqual action.type, 'connect'
        action.reject()

      new WSSocket "ws://localhost:#{@server.address().port}/websocket", (socket) ->
        socket.send({auth:'1234'});
        expectData socket, {auth:null, error:'forbidden'}, ->
          socket.onclose = ->
            test.done()

    'Authentication object available in auth function': (test) ->
      @auth = (agent, action) ->
        test.strictEqual agent.authentication.a, 1234
        test.strictEqual action.type, 'connect'
        action.reject()

      new WSSocket "ws://localhost:#{@server.address().port}/websocket", (socket) ->
        socket.send({auth:{a:1234}});
        expectData socket, {auth:null, error:'forbidden'}, ->
          socket.onclose = ->
            test.done()

  'Socket timeout if no auth message is sent': (test) ->
    new WSSocket "ws://localhost:#{@server.address().port}/websocket", (socket) ->
      socket.onmessage = (data) ->
        test.strictEqual data.auth, null
        test.strictEqual data.error, 'Timeout waiting for client auth message'
      socket.onclose = () ->
        test.done()